#!/usr/bin/env wish
#sync.tcl
#Description:
#
#Author: asteriskman
#Change log:
#10-Jan-2010 - Initial version
#18-Jan-2010 - Initial tree views in place which show local and master files
#Done:
#2 - find the local username and derived strings automatically
#1 - get config from somewhere else, a file would probably be nice
#9 - label the local and master tree views
#10 - don't show log window initially, but always store log info and display everything when log is opened
#6 - use menus to save config and do sync
#5 - use popup menus to select things to ignore
#8 - use different icons for ignored/normal folders/files
#14 - make the master file list say "loading" initially instead of just being empty
#18 - tell the user what files are to be synced before syncing
#17 - don't let the user try to sync before all data is loaded
#21 - support multiple sync profiles
#19 - actually sync the files
#29 - need to verify that a file was transfered correctly before moving it into place
#22 - update itunes library with new files
#12 - need progress bars (single file and full sync status) (provide % complete and eta)
#33 - pull images from a single zip file instead of multiple files
#
#To-do:
#3 - have a reasonable timeout for remote commands and detect and handle timeouts
#4 - allow the master machine password to be queried from user instead of coming from file
#7 - use different icons for open/closed folders
#11 - generate help info for the help menu
#13 - make the log have a maximum size so it doesn't explode and bog down the program
#15 - use the asterisk icon for the program icon
#16 - create a starpack for distribution
#20 - disable the main window widgets when the sync window is up
#23 - allow user to refresh trees & automatically refresh after a sync
#24 - if config file not found, don't use defaults, ask user for info
#25 - verify that remote temp dir exists before starting to push files
#26 - allow for only uploading or only downloading
#27 - update application code automatically with new version
#28 - remember which config was used last time and use as default
#29 - verify that the finalize command selected is valid
#30 - fix itunes add bug with videos
#31 - use a single dedicated command connection
#33 - if config is invalid tell the user and allow them to correct it

package require Ttk
package require tcom
package require vfs::zip

source ./configWindow.tcl
set configWindowDebug 0

#versions will be as follows:
#A.B.C
#A will be incremented only upon first public release and then after only on substantial re-writes
#B will be incremented when substantial function is changed/added, includes posibility of breaking older versions/setups
#C will be incremented at will but at least once per every version publicly released
set appVersion 0.1.2
set appName "asteriskman's sync"

set configFilePath $env(HOME)
#set configFileName [file join $env(HOME) ".amsync_config"]
set configList {master username password masterBasePath binPath localBasePath ignoreExtensions localTempDir masterTempDir finalizeCmd}

wm withdraw .

set logWindow ""
proc createLogWindow { lw } {
  global logWindow appName
  set logWindow $lw
  toplevel $logWindow
  wm withdraw $logWindow
  wm protocol $logWindow WM_DELETE_WINDOW "wm withdraw $logWindow"
  #wm transient $logWindow .
  text ${logWindow}.txtLog -font {Courier 8}
  place ${logWindow}.txtLog -relwidth 1.0 -relheight 1.0 -x 0 -y 0
  wm geometry $logWindow "800x400"
  wm title $logWindow "$appName log"
}

proc log { msg } {
  global logWindow
  ${logWindow}.txtLog insert end "$msg\n"
  ${logWindow}.txtLog see end
  update; update idletasks
}

proc error { msg } {
  log "ERROR: $msg"
}

proc popupError {msg} {
  global appName appVersion
  tk_messageBox -icon error -title "$appName $appVersion" -type ok -message "$msg"
}

#load configuration and return 1 on success, 0 if any errors
proc loadConfig { } {
  global CONFIG env configFilePath configFileName appName appVersion liHash miHash

  set configChoices [glob -nocomplain -tail -directory $configFilePath "*.amsync_config"]
  set numChoices [llength $configChoices]
  if {$numChoices == 0} {
    set configFileName [file join $configFilePath "default.amsync_config"]
    if {[string equal "yes" [tk_messageBox -icon question -parent . -title "$appName $appVersion" -type yesno -default yes -message "Config file $configFileName was not found.\nWould you like to use default settings?"]]} {
      set CONFIG(master) 192.168.128.10
      set CONFIG(username) rachel
      set CONFIG(password) ""
      set CONFIG(masterBasePath) /home/Rachel/music/
      set CONFIG(binPath) [file join $env(HOME) "My Documents" downloads]
      set CONFIG(localBasePath) [file join $env(HOME) "My Documents" "My Music" itunes "itunes music"]
      set CONFIG(ignoreExtensions) {.ini .jpg .skip .plist}
      set CONFIG(localTempDir) [file join $env(HOME) sync_temp]
      set CONFIG(masterTempDir) /home/Rachel/sync_temp
      set CONFIG(finalizeCmd) none
      saveConfig
    } else {
      return 0
    }
  } else {
    if {$numChoices == 1} {
      set configFileName [lindex $configChoices 0]
    } else {
      set configFileName [lindex $configChoices [getConfig .config [list [list option "Configuration to use?" $configChoices 0]]]]
    }
    set configFileName [file join $env(HOME) $configFileName]

    if {[catch {source $configFileName} fid]} {
      popupError "While reading config file:\n$fid"
      return 0
    }
  }

  #masterBasePath must end in a /
  if {[string index $CONFIG(masterBasePath) end] != "/"} {
    set CONFIG(masterBasePath) "$CONFIG(masterBasePath)/"
  }

  if {[string length $CONFIG(password)] == 0} {
    set pw [getConfig .password [list [list text "Password for $CONFIG(username)@$CONFIG(master)" ""]]]
    if {$pw == 0} {
      return 0
    } else {
      set CONFIG(password) $pw
    }
  }

  return 1
}

proc saveConfig { } {
  global CONFIG configFileName configList liHash miHash

  file delete ${configFileName}.backup
  file copy $configFileName ${configFileName}.backup

  if {[file exists $configFileName]} {
    file delete $configFileName
  }
  set fd [open $configFileName "w+"]
  foreach n $configList {
    puts $fd "set CONFIG($n) \{$CONFIG($n)\}"
  }

  if {[info exists liHash]} {
    puts $fd "array set liHash \{[array get liHash]\}"
  }
  if {[info exists miHash]} {
    puts $fd "array set miHash \{[array get miHash]\}"
  }

  close $fd
}

proc isLocalSetupCorrect { } {
  global CONFIG configList

  foreach n $configList {
    if {![info exists CONFIG($n)]} {
      popupError "CONFIG($n) not defined in config"
      return 0
    }
  }

  foreach p {binPath localBasePath} {
    if {![file exists $CONFIG($p)]} {
      popupError "$p $CONFIG($p) does not exist"
      return 0
    }
  }

  set requiredBinFiles {plink.exe pscp.exe}
  foreach f $requiredBinFiles {
    set fileName [file join $CONFIG(binPath) $f]
    if {![file exists $fileName]} {
      popupError "$f does not exist at $fileName"
      return 0
    }
  }
  return 1
}

proc getRemoteDirList { } {
  global CONFIG
  set command "/usr/bin/find $CONFIG(masterBasePath) -type d -printf \"%P\\r\\n\""
  log "Getting directory list"
  set fd [open "| \"$CONFIG(binPath)/plink\" -C -l $CONFIG(username) -pw $CONFIG(password) $CONFIG(master) $command"]
  fconfigure $fd -translation auto -buffering full -blocking 0
  global result
  set result {}
  fileevent $fd readable "getData $fd result"
  #set rc [catch {set output [exec $CONFIG(binPath)/plink -l $CONFIG(username) -pw $CONFIG(password) $CONFIG(master) $command]} msg]
  #log "RC=$rc"
  #if {$rc != 0} {
  #  #log "$output"
  #  log "$msg"
  #  return
  #}
  #log $output
}

proc getRemoteFileList { } {
  global CONFIG
  set command "/usr/bin/find $CONFIG(masterBasePath) -type f -printf \"%P:%s\\r\\n\""
  set fd [open "| \"$CONFIG(binPath)/plink\" -C -l $CONFIG(username) -pw $CONFIG(password) $CONFIG(master) $command"]
  fconfigure $fd -translation auto -buffering full -blocking 0
  global result
  set result {}
  fileevent $fd readable "getData $fd result"
  #set output [read $fd]
  #close $fd
  #set rc [catch {set output [exec $CONFIG(binPath)/plink -l $CONFIG(username) -pw $CONFIG(password) $CONFIG(master) $command]} msg]
  #log "RC=$rc"
  #if {$rc != 0} {
  #  #log "$output"
  #  log "$msg"
  #  return
  #}
  #log $output
}

proc remoteTreeDone { } {
  log "Remote tree done"
}

proc getData {fd rName} {
  upvar #0 $rName r
  fileevent $fd readable ""
  if {![eof $fd]} {
    append r [read $fd]
    fileevent $fd readable "getData $fd $rName"
  } else {
    global doneCmd
    close $fd
    #log "got data \"$r\""
    eval $doneCmd
  }
}

proc updateRemoteTreeDirs {t} {
  global result remotePathHash doneCmd miHash
  array unset remotePathHash
  #if {[llength $result] == 0} {
  #  popupError "Unable to retreive remote directory list"
  #  return 0
  #}

  set result [string trimright $result]
  foreach p [lrange [split $result \n] 1 end] {
    if {[string length $p] == 0} {continue}
    set dirname [file dirname $p]
    if {[string equal $dirname .]} {
      if {[info exists miHash([string tolower $p])]} {
        set node [$t insert {} end -text $p -values [list "y" $p dir] -tags {basic} -image ::img::folder_closed_ignore]
      } else {
        set node [$t insert {} end -text $p -values [list "" $p dir] -tags {basic} -image ::img::folder_closed_normal]
      }
      set remotePathHash($p) $node
    } else {
      set parent $remotePathHash($dirname)
      if {[info exists miHash([string tolower $p])]} {
        set node [$t insert $parent end -text [file tail $p] -values [list "y" $p dir] -tags {basic} -image ::img::folder_closed_ignore]
        set pnode $node
        while {[set pnode [$t parent $pnode]] != {}} {
          foreach {pignore ppath ptype} [$t item $pnode -values] {break}
          if {$pignore == ""} {
            $t item $pnode -values [list "?" $ppath $ptype]
            $t item $pnode -image ::img::folder_closed_child_ignore
          }
        }
      } else {
        set node [$t insert $parent end -text [file tail $p] -values [list "" $p dir] -tags {basic} -image ::img::folder_closed_normal]
      }
      set remotePathHash($p) $node
    }
  }
  set doneCmd {updateRemoteTreeFiles .mTree}
  getRemoteFileList
}

proc updateRemoteTreeFiles {t} {
  global result remotePathHash miHash mLabelNode mfHash CONFIG msHash
  #if {[llength $result] == 0} {
  #  popupError "Unable to retreive remote file list"
  #  return 0
  #}
  set result [string trimright $result]
  foreach p [split $result \n] {
    foreach {name size} [split $p :] {break}
    set p $name
    set msHash([string tolower $p]) $size
    if {[string length $p] == 0} {continue}
    set dirname [file dirname $p]
    if {[string equal $dirname "."]} {
      set parent {}
    } else {
      set parent $remotePathHash($dirname)
    }
    if {[lsearch -ascii -exact $CONFIG(ignoreExtensions) [file extension $p]] == -1} {
      set mfHash([string tolower $p]) $p
      if {[info exists miHash([string tolower $p])]} {
        set node [$t insert $parent end -text [file tail $p] -values [list "y" $p file] -tags {basic} -image ::img::file_ignore]
        set pnode $node
        while {[set pnode [$t parent $pnode]] != {}} {
          foreach {pignore ppath ptype} [$t item $pnode -values] {break}
          if {$pignore == ""} {
            $t item $pnode -values [list "?" $ppath $ptype]
            $t item $pnode -image ::img::folder_closed_child_ignore
          }
        }
      } else {
        set node [$t insert $parent end -text [file tail $p] -values [list "" $p file] -tags {basic} -image ::img::file_normal]
      }
    } else {
      #this case is for when the file matches the ignoreExtensions list
    }
  }

  $t delete $mLabelNode
  .mMain entryconfigure Sync -state normal
}

proc updateLocalTree {t {path ""} {parent {}}} {
  global CONFIG liHash lfHash lLabelNode

  #this code should probably be the other way around(files first, then dirs)...lack of foresight

  set fsPath [file join $CONFIG(localBasePath) $path]

  foreach p [glob -nocomplain -tails -type d -directory $fsPath "*"] {
    set fullPath [file join $path $p]
    set fsFullPath [file join $fsPath $p]
    if {[info exists liHash([string tolower $fullPath])]} {
      set node [$t insert $parent end -text $p -values [list "y" $fullPath dir] -tags {basic} -image {::img::folder_closed_ignore}]
      #we need to udpate the parents here
      set pnode $node
      while {[set pnode [$t parent $pnode]] != {}} {
        foreach {pignore ppath ptype} [$t item $pnode -values] {break}
        if {$pignore == ""} {
          $t item $pnode -values [list "?" $ppath $ptype]
          $t item $pnode -image ::img::folder_closed_child_ignore
        }
      }

    } else {
      set node [$t insert $parent end -text $p -values [list "" $fullPath dir] -tags {basic} -image {::img::folder_closed_normal}]
    }
    #$t insert $node 0 -text dummy ;# a dummy
    updateLocalTree $t $fullPath $node
    foreach f [glob -nocomplain -tails -type f -directory $fsFullPath "*"] {
      set fullFilePath [file join $fullPath $f]
      if {[lsearch -ascii -exact $CONFIG(ignoreExtensions) [file extension $f]] == -1} {
        set lfHash([string tolower $fullFilePath]) $fullFilePath
        if {[info exists liHash([string tolower $fullFilePath])]} {
          set cnode [$t insert $node end -text $f -values [list "y" $fullFilePath file] -tags {basic} -image {::img::file_ignore}]
          set pnode $cnode
          while {[set pnode [$t parent $pnode]] != {}} {
            foreach {pignore ppath ptype} [$t item $pnode -values] {break}
            if {$pignore == ""} {
              $t item $pnode -values [list "?" $ppath $ptype]
              $t item $pnode -image ::img::folder_closed_child_ignore
            }
          }
        } else {
          $t insert $node end -text $f -values [list "" $fullFilePath file] -tags {basic} -image {::img::file_normal}
        }
      } else {
        #this case is for when the file matches the ignoreExtensions list
      }
    }
  }

  if {$parent == {}} {
    foreach f [glob -nocomplain -tails -type f -directory $fsPath "*"] {
      set fullFilePath [file join $path $f]
      if {[lsearch -ascii -exact $CONFIG(ignoreExtensions) [file extension $f]] == -1} {
        set lfHash([string tolower $fullFilePath]) $fullFilePath
        if {[info exists liHash([string tolower $fullFilePath])]} {
          $t insert {} end -text $f -values [list "y" $fullFilePath file] -tags {basic} -image {::img::file_ignore}
        } else {
          $t insert {} end -text $f -values [list "" $fullFilePath file] -tags {basic} -image {::img::file_normal}
        }
      } else {
        #this case is for when the file matches the ignoreExtensions list
      }
    }
  }

}

proc itemIgnore {w hashName x y} {
  upvar #0 $hashName hash
  log "LI [$w item [$w identify row $x $y]]"
  set item [$w identify row $x $y]
  foreach {ignore path type} [$w item $item -values] {break}
  set hash([string tolower $path]) 1
  saveConfig
  #now we have to reflect this in the tree
  $w item $item -values [list y $path $type]
  switch -- $type {
    dir {
      $w item $item -image ::img::folder_closed_ignore
    }
    file {
      $w item $item -image ::img::file_ignore
    }
  }
  while {[set item [$w parent $item]] != {}} {
    foreach {ignore path type} [$w item $item -values] {break}
    if {$ignore != y} {
      $w item $item -image ::img::folder_closed_child_ignore
      $w item $item -values [list ? $path $type]
    }
  }
  #we might also want to do the children..but that's more difficult, maybe not required
}
proc itemUnIgnore {w hashName x y} {
  upvar #0 $hashName hash
  log "LU [$w item [$w identify row $x $y]]"
  set item [$w identify row $x $y]
  foreach {ignore path type} [$w item $item -values] {break}
  if {[info exists hash([string tolower $path])]} {
    array unset hash "[string tolower $path]"
  }
  saveConfig
  switch -- $type {
    dir {
      if {[anyChildrenIgnored $w $item]} {
        $w item $item -values [list "?" $path $type]
        $w item $item -image ::img::folder_closed_child_ignore
      } else {
        $w item $item -values [list "" $path $type]
        $w item $item -image ::img::folder_closed_normal
      }
    }
    file {
      $w item $item -values [list "" $path $type]
      $w item $item -image ::img::file_normal
    }
  }

  while {[set item [$w parent $item]] != {}} {
    set ci [anyChildrenIgnored $w $item]
    foreach {ignore path type} [$w item $item -values] {break}
    if {$ignore == "?" && $ci == 0} {
      $w item $item -image ::img::folder_closed_normal
      $w item $item -values [list "" $path $type]
    }
  }
}

proc anyChildrenIgnored {w item} {
  set children [$w children $item]
  foreach c $children {
    foreach {ignore path type} [$w item $c -values] {break}
    if {$ignore == "y" || [anyChildrenIgnored $w $c]} {
      return 1
    }
  }
  return 0
}

proc masterIgnore {x y} {
  global miHash
  log "MI [.mTree item [.mTree identify row $x $y]]"
  set item [
}
proc masterUnIgnore {x y} {
  log "MU [.mTree item [.mTree identify row $x $y]]"
}

proc GUIexit {} {
  global appName appVersion
  if {[string equal "yes" [tk_messageBox -icon question -parent . -title "$appName $appVersion" -type yesno -default yes -message "Are you sure you want to quit?"]]} {
    saveConfig
    exit 0
  }
}

proc configGUI { } {
  global liHash miHash configList CONFIG
  foreach n [array names liHash] {
    log "liHash($n) = $liHash($n)"
  }
  foreach n [array names miHash] {
    log "miHash($n) = $miHash($n)"
  }
  set configGUIList {}
  foreach n $configList {
    lappend configGUIList [list text $n $CONFIG($n)]
  }
  set result [getConfig .config $configGUIList]
  log $result
  if {$result != 0} {
    foreach name $configList value $result {
      set CONFIG($name) $value
    }
    saveConfig
  }
}

proc showLog { } {
  global logWindow
  wm deiconify $logWindow
}

proc GUIhelp { } {
  global appName appVersion
  tk_messageBox -icon info -parent . -title "$appName $appVersion" -type ok -message "Sorry. Help is not implemented yet."
}

proc GUIsync { } {
  global miHash liHash mfHash lfHash global pushFiles pullFiles appName appVersion
  #determine which files are to be synced in each direction
  set pullFiles {}
  set pushFiles {}
  foreach f [array names lfHash] {
    set f $lfHash($f)
    set lf [string tolower $f]
    if {![info exists mfHash($lf)] && ![info exists liHash($lf)]} {
     set dirname $lf
     set ignoredViaParent 0
     while {[set dirname [file dirname $dirname]] != "."} {
       if {[info exists liHash($dirname)]} {
         set ignoredViaParent 1
         break
       }
     }
     if {!$ignoredViaParent} {
       lappend pushFiles $f
     }
    }
  }
  set pushFiles [lsort -ascii $pushFiles]
  foreach f [array names mfHash] {
    set f $mfHash($f)
    set lf [string tolower $f]
    if {![info exists lfHash($lf)] && ![info exists miHash($lf)]} {
      set dirname $lf
      set ignoredViaParent 0
      while {[set dirname [file dirname $dirname]] != "."} {
        if {[info exists miHash($dirname)]} {
          set ignoredViaParent 1
          break
        }
      }
      if {!$ignoredViaParent} {
        lappend pullFiles $f
      }
    }
  }
  set pullFiles [lsort -ascii $pullFiles]

  log "push/pull files calculated"

  #present the sync info window
  set w .syncreview
  toplevel $w

  label ${w}.lPull -text "Files to download ([llength $pullFiles])"
  label ${w}.lPush -text "Files to upload ([llength $pushFiles])"
  text ${w}.tPull -xscroll "${w}.hsbPull set" -yscroll "${w}.vsbPull set" -wrap none
  text ${w}.tPush -xscroll "${w}.hsbPush set" -yscroll "${w}.vsbPush set" -wrap none
  ttk::scrollbar ${w}.hsbPull -orient horizontal -command "${w}.tPull xview"
  ttk::scrollbar ${w}.vsbPull -orient vertical -command "${w}.tPull yview"
  ttk::scrollbar ${w}.hsbPush -orient horizontal -command  "${w}.tPush xview"
  ttk::scrollbar ${w}.vsbPush -orient vertical -command "${w}.tPush yview"
  entry ${w}.eStatus
  ttk::progressbar ${w}.pFile -maximum 100
  ttk::progressbar ${w}.pTask -maximum [expr "[llength $pullFiles] + [llength $pushFiles]"]
  button ${w}.bStartSync -text "Start Sync" -command "startSync $w"
  button ${w}.bStopSync -text "Stop Sync" -command "stopSync $w" -state disabled

  place ${w}.lPull -x 0 -y 0
  place ${w}.lPush -relx 0.5 -y 0
  update
  set labelHeight [winfo height ${w}.lPull]
  place ${w}.tPull -x 0 -y $labelHeight -relwidth 0.5 -width -20 -relheight 1.0 -height [expr {-($labelHeight + 110)}]
  place ${w}.tPush -relx 0.5 -y $labelHeight -relwidth 0.5 -width -20 -relheight 1.0 -height [expr {-($labelHeight + 110)}]
  place ${w}.hsbPull -x 0 -rely 1.0 -y -110 -height 20 -relwidth 0.5 -width -20
  place ${w}.vsbPull -relx 0.5 -x -20 -y $labelHeight -relheight 1.0 -height [expr {-($labelHeight + 110)}] -width 20
  place ${w}.hsbPush -relx 0.5 -rely 1.0 -y -110 -height 20 -relwidth 0.5 -width -20
  place ${w}.vsbPush -relx 1.0 -x -20 -y $labelHeight -relheight 1.0 -height [expr {-($labelHeight + 110)}] -width 20
  place ${w}.bStartSync -x 0 -rely 1.0 -y -90 -height 30 -relwidth 0.5
  place ${w}.bStopSync -relx 0.5 -rely 1.0 -y -90 -height 30 -relwidth 0.5
  place ${w}.eStatus -x 0 -rely 1.0 -y -60 -height 20 -relwidth 1.0
  place ${w}.pFile -x 0 -rely 1.0 -y -40 -height 20 -relwidth 1.0
  place ${w}.pTask -x 0 -rely 1.0 -y -20 -height 20 -relwidth 1.0

  #set pushFiles {}
  #set pullFiles [list [lindex $pullFiles 0]]

  foreach f $pullFiles {
    ${w}.tPull insert end "  $f\n"
  }
  foreach f $pushFiles {
    ${w}.tPush insert end "  $f\n"
  }

  wm title $w "$appName (Sync Status)"
  wm geometry $w "1000x500"
  #wm protocol $w WM_DELETE_WINDOW
  focus $w
  wm transient $w .

  #\u221a is a check mark
  #\u25ba is a right pointing arrow
}

proc startSync {w} {
  global pullFiles pushFiles env global tempDir CONFIG stopSync masterCmdDone appName appVersion msHash

  ${w}.bStartSync configure -state disabled
  ${w}.bStopSync configure -state active

  #clean up any old temp files
  set tempDir $CONFIG(localTempDir)
  file delete -force $tempDir
  file mkdir $tempDir
  set stopSync 0

  #start the sync process
  #first do the downloads
  foreach file $pullFiles {
    if {!$stopSync} {
      ${w}.pFile configure -value 0
      ${w}.eStatus delete 0 end
      ${w}.eStatus insert 0 "Downloading $file"
      set baseFilename "$file"
      set localFinalPath [file join $CONFIG(localBasePath) [file dirname $file]]
      set localTempFile [file join $tempDir [file tail $file]]
      file delete $localTempFile
      file mkdir $localFinalPath
      set file "$CONFIG(masterBasePath)/[string map {& {\\&} ( {\\(} ) {\\)} ' {\\'} { } {\\ } [ {\[} ] {\]}} $file]"

      set remoteString "$CONFIG(master):$file"
               log "| \"$CONFIG(binPath)/pscp\" -scp -C -p -l $CONFIG(username) -pw $CONFIG(password) \"$remoteString\" \"$localTempFile\""
      set fd [open "| \"$CONFIG(binPath)/pscp\" -scp -C -p -l $CONFIG(username) -pw $CONFIG(password) \"$remoteString\" \"$localTempFile\""]
      set pid [pid $fd]
      fconfigure $fd -blocking 0 -buffering line
      fileevent $fd readable "getDownloadStatus $w $fd \"[string map {[ {\[}} $baseFilename]\" Downloading"
      global fileDone
      set fileDone 0
      tkwait variable fileDone
      if {$stopSync} {
        catch {close $fd}
        exec taskkill /F /PID $pid
      } else {
        #not sure what/how to check for errors here
        if {[file exists $localTempFile]} {
          set localFileSize [file size $localTempFile]
          set remoteFileSize $msHash([string tolower $baseFilename])
          if {$localFileSize == $remoteFileSize} {
            ${w}.eStatus delete 0 end
            ${w}.eStatus insert 0 "Finalizing $baseFilename"
            #if {[catch {file rename -- $localTempFile $localFinalPath} fid]} {
            #  log "could not rename $localTempFile to $localFinalPath\n$fid"
            #} else {
            #  finalizeCmd $localFinalPath $CONFIG(finalizeCmd)
            #}
            finalizeCmd $localTempFile $CONFIG(finalizeCmd)
          } else {
            log "$file not downloaded correctly"
          }
        } else {
          log "$file not downloaded correctly"
        }
      }
      ${w}.pTask step 1
    }
  }
  #this fails if the file is still opened by pscp
  #if {[llength $pullFiles]} {
  #  #clean up any temp files left
  #  file delete -force $tempDir
  #}


  set tempDir $CONFIG(masterTempDir)
  set masterCmdDone -1
  executeMasterCmd "/usr/bin/mkdir -p $tempDir"
  tkwait variable masterCmdDone
  foreach file $pushFiles {
    if {!$stopSync} {
      ${w}.pFile configure -value 0
      ${w}.eStatus delete 0 end
      ${w}.eStatus insert 0 "Uploading $file"
      set baseFilename "$file"
      set masterFinalPath [file join $CONFIG(masterBasePath) [file dirname $file]]
      set masterTempFile [file join $tempDir [file tail $file]]
      set masterCmdDone -1
      executeMasterCmd "/usr/bin/rm $masterTempFile"
      tkwait variable masterCmdDone

      #set file "$CONFIG(localBasePath)/[string map {[ {\[} ] {\]}} $file]"
      set file "$CONFIG(localBasePath)/$file"
      set masterTempFile [string map {& {\\&} + {\\+} { } {\\ } ( {\\(} ) {\\)} ' {\\'} [ {\[} ] {\]} } $masterTempFile]
      #log "Copying $file to $masterTempFile and move to $masterFinalPath"
      log "| \"$CONFIG(binPath)/pscp\" -scp -C -p -l $CONFIG(username) -pw $CONFIG(password) \"$file\" \"$CONFIG(master):$masterTempFile\""
      set fd [open "| \"$CONFIG(binPath)/pscp\" -scp -p -l $CONFIG(username) -pw $CONFIG(password) \"$file\" \"$CONFIG(master):$masterTempFile\""]
      set pid [pid $fd]
      fconfigure $fd -blocking 0 -buffering line
      log "about to do fileevent"
      #log ">about to do \"fileevent $fd readable \"getDownloadStatus $w $fd \\\"$baseFilename\\\" Uploading\"\""
      fileevent $fd readable "getDownloadStatus $w $fd \"[string map {[ {\]}} $baseFilename]\" Uploading"
      global fileDone
      set fileDone 0
      tkwait variable fileDone
      log "tkwait done"
      if {$stopSync} {
        catch {close $fd}
        exec taskkill /F /PID $pid
      } else {
        set localFileSize [file size $file]

        set masterCmdDone -1
        executeMasterCmd "/usr/bin/stat -c %s \"$masterTempFile\""
        tkwait variable masterCmdDone
        if {$masterCmdDone == $localFileSize} {
          ${w}.eStatus delete 0 end
          ${w}.eStatus insert 0 "Finalizing $baseFilename"
          set masterCmdDone -1
          set masterFinalPath [string map {& {\\&} + {\\+} { } {\\ } ( {\\(} ) {\\)} ' {\\'} [ {\[} ] {\]} } $masterFinalPath]
          #log "/usr/bin/mkdir -p \"$masterFinalPath\""
          executeMasterCmd "/usr/bin/mkdir -p \"$masterFinalPath\""
          tkwait variable masterCmdDone
          #do something here if mkdir fails
          #log "$masterCmdDone"

          set masterCmdDone -1
          executeMasterCmd "/usr/bin/mv \"$masterTempFile\" \"$masterFinalPath\""
          tkwait variable masterCmdDone
        } else {
          log "$file not uploaded correctly"
        }
      }
      ${w}.pTask step 1
    }
  }

  if {[llength $pushFiles]} {
    set masterCmdDone -1
    executeMasterCmd "/usr/bin/rm \"$tempDir/*\""
    tkwait variable masterCmdDone
  }

  #alert the user that the sync is complete
  ${w}.pTask configure -maximum 100
  ${w}.pTask configure -value 100
  ${w}.eStatus delete 0 end
  if {$stopSync} {
    ${w}.eStatus insert 0 "Sync stopped"
  } else {
    ${w}.eStatus insert 0 "Sync complete"
    tk_messageBox -icon info -title "$appName $appVersion" -type ok -message "Sync completed."
  }
}

proc executeMasterCmd {cmd} {
  global CONFIG doneCmd
  set fd [open "| \"$CONFIG(binPath)/plink\" -C -l $CONFIG(username) -pw $CONFIG(password) $CONFIG(master) $cmd"]
  fconfigure $fd -translation auto -buffering full -blocking 0
  global result
  set result {}
  set doneCmd {global masterCmdDone result; set masterCmdDone $result}
  fileevent $fd readable "getData $fd result"

}

proc getDownloadStatus {w fd file mode} {
  global fileDone
  fileevent $fd readable ""
  if {![eof $fd]} {
    set data [string trimright [read $fd]]
    if {[string length $data] > 0} {
      #log "\"$data\""
      #words                     |          4 kB |   4.0 kB/s | ETA: 00:00:47 |   2%
      #words                     |        193 kB | 193.9 kB/s | ETA: 00:00:00 | 100%
      set lastLine [lindex [split $data "\n"] end]
      foreach {name totalSize rate eta percent} [split $lastLine |] {break}
      ${w}.eStatus delete 0 end
      ${w}.eStatus insert 0 "$mode $file $rate $eta"
      ${w}.pFile configure -value [string trimright $percent %]
    }
    fileevent $fd readable "getDownloadStatus $w $fd \"[string map {[ {\[}} $file]\" $mode"
  } else {
    close $fd
    #log "ds done"
    set fileDone 1
  }
}

proc stopSync {w} {
  global stopSync fileDone
  set stopSync 1
  set fileDone 1
}

proc finalizeCmd {file cmd} {
  switch -- $cmd {
    addToItunes {
      global iTunesLibrary
      if {![info exists iTunesLibrary]} {
        log "Opening iTunes"
        set iTunesLibrary [[::tcom::ref createobject iTunes.Application] LibraryPlaylist]
      }
      set rc [$iTunesLibrary AddFile $file]
    }
    none {
    }
    default {
    }
  }
}

proc createGUI { } {
  global mLabelNode lLabelNode
  wm protocol . WM_DELETE_WINDOW GUIexit

  label .mLabel -text "Master files"
  place .mLabel -x 0 -y 0
  update
  set mLabelHeight [winfo height .mLabel]

  ttk::treeview .mTree -columns {ignore path type} -displaycolumns {ignore} -yscroll ".ltvsb set" -xscroll ".lthsb set"
  .mTree heading \#0 -text "Path"
  .mTree column \#0 -stretch 1
  .mTree heading ignore -text "Ignore"
  .mTree column ignore -stretch 0 -width 80
  .mTree tag configure basic -background "#FFFFFF"

  set mLabelNode [.mTree insert {} end -text "Please wait - downloading master file list" -tags {basic}]

  ttk::scrollbar .ltvsb -orient vertical -command ".mTree yview"
  ttk::scrollbar .lthsb -orient horizontal -command ".mTree xview"

  place .mTree -x 0 -y $mLabelHeight -relwidth 1.0 -relheight 0.5 -width -20 -height -20
  place .ltvsb -x -20 -y $mLabelHeight -relx 1.0 -width 20 -relheight 0.5 -height [expr {-(20+$mLabelHeight)}]
  place .lthsb -x 0 -y -20 -rely 0.5 -height 20 -relwidth 1.0 -width -20


  label .lLabel -text "Local files"
  place .lLabel -x 0 -rely 0.5
  update
  set lLabelHeight [winfo height .lLabel]

  ttk::treeview .lTree -columns {ignore path type} -displaycolumns {ignore} -yscroll ".mtvsb set" -xscroll ".mthsb set"
  .lTree heading \#0 -text "Path"
  .lTree column \#0 -stretch 1
  .lTree heading ignore -text "Ignore"
  .lTree column ignore -stretch 0 -width 80
  .lTree tag configure basic -background "#FFFFFF"

  set lLabelNode [.lTree insert {} end -text "Please wait - reading local file list" -tags {basic}]

  ttk::scrollbar .mtvsb -orient vertical -command ".lTree yview"
  ttk::scrollbar .mthsb -orient horizontal -command ".lTree xview"

  place .lTree -x 0 -y $lLabelHeight -rely 0.5 -relwidth 1.0 -relheight 0.5 -width -20 -height -20
  place .mtvsb -x -20 -y $lLabelHeight -rely 0.5 -relx 1.0 -width 20 -relheight 0.5 -height [expr {-(20+$lLabelHeight)}]
  place .mthsb -x 0 -y -20 -rely 1.0 -height 20 -relwidth 1.0 -width -20

  bind .lTree <<TreeviewOpen>> {log "%W [%W focus]"}
  #bind .lTree <Button-3> {log "[%W item [%W identify row %x %y]]"}
  bind .lTree <Button-3> {set popupX %x; set popupY %y; tk_popup .popupLocal %X %Y}
  bind .mTree <Button-3> {set popupX %x; set popupY %y; tk_popup .popupMaster %X %Y}

  menu .mMain
  .mMain add command -label "Config" -underline 0 -command {configGUI}
  .mMain add command -label "Sync" -underline 0 -command {GUIsync} -state disabled
  .mMain add command -label "Log" -underline 0 -command {showLog}
  .mMain add command -label "Exit" -underline 1 -command {GUIexit}
  .mMain add command -label "Help" -underline 0 -command {GUIhelp}
  . configure -menu .mMain

  menu .popupLocal
  .popupLocal add command -label "Ignore" -command {itemIgnore .lTree liHash $popupX $popupY}
  .popupLocal add command -label "Un-Ignore" -command {itemUnIgnore .lTree liHash $popupX $popupY}
  menu .popupMaster
  .popupMaster add command -label "Ignore" -command {itemIgnore .mTree miHash $popupX $popupY}
  .popupMaster add command -label "Un-Ignore" -command {itemUnIgnore .mTree miHash $popupX $popupY}

  wm geometry . "500x800"
  global appName appVersion
  wm title . "$appName $appVersion"

  #get images for dir and files
  set img_zip_file [vfs::zip::Mount sync_images.zip sync_images.zip]
  image create photo ::img::folder_closed_normal -file sync_images.zip/folder_closed_normal.gif
  image create photo ::img::folder_closed_ignore -file sync_images.zip/folder_closed_ignore.gif
  image create photo ::img::folder_closed_child_ignore -file sync_images.zip/folder_closed_child_ignore.gif
  image create photo ::img::file_normal -file sync_images.zip/file_normal.gif
  image create photo ::img::file_ignore -file sync_images.zip/file_ignore.gif

  createLogWindow ".logWindow"
  log "Welcome to $appName version $appVersion"

  bind . <F8> {source ./attachConsole.tcl}

  wm deiconify .
}



createGUI
if {[loadConfig]} {
  if {[isLocalSetupCorrect]} {
    #createGUI
    updateLocalTree .lTree
    .lTree delete $lLabelNode
    set doneCmd {updateRemoteTreeDirs .mTree}
    getRemoteDirList
  } else {
    popupError "Local setup is incorrect."
    exit 1
  }
} else {
  popupError "Unable to read config."
  exit 1
}

